<template>
  <div class="rest-image-cropper">
    <!-- 上传弹窗 -->
    <el-dialog v-model="showDialog" :title="title" width="760px" :beforeClose="hide" draggable destroyOnClose>
      <div class="container">
        <!-- 左侧裁剪区 -->
        <div class="left-box">
          <div class="cropper-box">
            <img v-if="imgUrl" ref="imageRef" :src="imgUrl" class="image-box" />
            <div v-else ref="imageRef" class="image-box" @click="chooseImage"></div>
          </div>
          <div class="tool-box">
            <el-button @click="chooseImage">更换图片</el-button>
            <el-button @click="zoomImage(0.2)">放大</el-button>
            <el-button @click="zoomImage(-0.2)">缩小</el-button>
            <el-button @click="rotateImage(90)">左转90°</el-button>
            <el-button @click="rotateImage(90)">右转90°</el-button>
          </div>
        </div>
        <!-- 右侧预览区 -->
        <div class="right-box">
          <!-- 小图预览区域 -->
          <div id="image-preview" class="image-preview"></div>
          <div class="image-info-box">
            <div v-if="cropperWidth !== null" style="margin-bottom: 20px">
              <span>宽度：{{ cropperWidth }}px</span>
            </div>
            <div v-if="cropperHeight !== null">
              <span>高度：{{ cropperHeight }}px</span>
            </div>
          </div>
        </div>
      </div>
      <!-- 只用input来实现上传，但是不显示input -->
      <input ref="fileRef" style="visibility: hidden" type="file" accept="image/png, image/jpeg" @change="selectImage" />
      <template #footer>
        <el-button @click="hide">取消</el-button>
        <el-button type="primary" @click="submit">确定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
  import 'cropperjs/dist/cropper.css';
  import Cropper from 'cropperjs';
  export type FileType = keyof typeof fileTypeObj; // eslint-disable-line no-use-before-define
  export type CroppedCanvasOptions = Cropper.GetCroppedCanvasOptions & {
    /** 最终生成图片的宽度 */
    width?: number;
    /** 最终生成图片的高度 */
    height?: number;
    /** 最终生成图片的最小宽度 */
    minWidth?: number;
    /** 最终生成图片的最小高度 */
    minHeight?: number;
    /** 最终生成图片的最大宽度 */
    maxWidth?: number;
    /** 最终生成图片的最大高度 */
    maxHeight?: number;
    /** 设置为true以使用舍入值(裁剪区域的位置和大小数据)，默认值为false */
    rounded?: boolean;
    /** 最终生成图片的背景填充色 */
    fillColor?: string;
    /** 用来设置图片是否平滑的属性，详情：https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled */
    imageSmoothingEnabled?: boolean;
    /** 用于设置图像平滑度的属性，有兼容性问题，详情：https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/imageSmoothingQuality */
    imageSmoothingQuality?: ImageSmoothingQuality;
    /** 生成图片的类型 */
    fileType?: FileType;
    /** 生成图片的文件名 */
    fileName?: `${string}.${FileType}`;
    /** 清晰度 0 到 1 注意 png 图片无效 */
    quality?: number;
  };
  export type Props = {
    /** 默认图片 (可以是 File 对象或者一个路径) */
    file?: File | string;
    /** 弹框标题 */
    title?: string;
    /** 生成图片的配置 */
    croppedCanvasOptions?: CroppedCanvasOptions;
    /** 裁剪配置 */
    cropperOptions?: {
      /** 设置裁剪框为固定的宽高比 */
      aspectRatio?: number;
      /** 允许初始化时自动的裁剪图片 配合 data 使用 默认true */
      autoCrop?: boolean;
      /** 设置裁剪区域占图片的大小 值为 0-1 默认 0.8 表示 80%的区域 */
      autoCropArea?: number;
      /** 是否在容器内显示网格状的背景 默认true */
      background?: boolean;
      /** 是否显示裁剪框中间的 ‘+’ 指示器 默认true */
      center?: boolean;
      /** 检查图片是否跨域 默认true 如果是 会在被复制的图片元素上加上属性crossOrigin 并且在src上加上一个时间戳 避免重加载图片时因为浏览器缓存而加载错误 */
      checkCrossOrigin?: boolean;
      /** 检查图片的方向信息（仅JPEG图片有）默认true 在旋转图片时会对图片方向值做一些处理 以解决IOS设备上的一些问题 */
      checkOrientation?: boolean;
      /** 是否可以拖拽裁剪框 默认true */
      cropBoxMovable?: boolean;
      /** 是否可以改变裁剪框的尺寸 默认true */
      cropBoxResizable?: boolean;
      /** 之前存储的裁剪后的数据 在初始化时会自动设置 只有在autoCrop设置为true时可用 */
      data?: Cropper.SetDataOptions;
      /** 拖拽图片模式, crop 形成新的裁剪框; move 图片可移动; none 什么也没有 */
      dragMode?: Cropper.DragMode;
      /** 是否显示裁剪框的虚线 默认true */
      guides?: boolean;
      /** 是否显示裁剪框上面的白色蒙版 （很淡）默认true */
      highlight?: boolean;
      /** 裁剪框宽高比的初始值 默认与图片宽高比相同 只有在aspectRatio没有设置的情况下可用 */
      initialAspectRatio?: number;
      /** 画布的最小高度 默认0 */
      minCanvasHeight?: number;
      /** 画布的最小宽度 默认0 */
      minCanvasWidth?: number;
      /** 容器的最小 高度 默认200 */
      minContainerHeight?: number;
      /** minCanvasWidth */
      minContainerWidth?: number;
      /** 裁剪框小高度 默认0 */
      minCropBoxHeight?: number;
      /** 裁剪框小宽度 默认0 */
      minCropBoxWidth?: number;
      /** 是否显示图片和裁剪框之间的黑色蒙版 默认true */
      modal?: boolean;
      /** 是否可以移动图片 默认true */
      movable?: boolean;
      /** 预览 设置一个区域容器预览裁剪后的结果 */
      preview?: HTMLElement | HTMLElement[] | NodeListOf<HTMLElement> | string;
      /** 在窗口尺寸调整后 进行响应式的重渲染 默认true */
      responsive?: boolean;
      /** 在窗口尺寸调整后 恢复被裁剪的区域 默认true */
      restore?: boolean;
      /** 是否可以旋转图片 默认true */
      rotatable?: boolean;
      /** 是否可以缩放图片（可以改变长宽） 默认true */
      scalable?: boolean;
      /** 是否可以通过双击切换拖拽图片模式（move和crop）默认true 当拖拽图片模式为none时不可切换 该设置必须浏览器支持双击事件 */
      toggleDragModeOnDblclick?: boolean;
      /** 裁剪模式 0 无限制;1 限制裁剪框不能超出图片的范围;2 限制裁剪框不能超出图片的范围 且图片填充模式为 cover 最长边填充;3 限制裁剪框不能超出图片的范围 且图片填充模式为 contain 最短边填充 */
      viewMode?: Cropper.ViewMode;
      /** 设置鼠标滚轮缩放的灵敏度 默认 0.1 */
      wheelZoomRatio?: number;
      /** 是否可以通过拖拽触摸缩放图片 默认true */
      zoomOnTouch?: boolean;
      /** 是否可以通过鼠标滚轮缩放图片 默认true */
      zoomOnWheel?: boolean;
      /** 是否可以缩放图片（改变焦距） 默认true */
      zoomable?: boolean;
    };
  };

  export type ShowParam = Props & {
    /** 成功回调 */
    success: (file: File) => void;
    /** 失败回调 */
    fail: () => void;
  };
  const props = withDefaults(defineProps<Props>(), {
    file: undefined, // 默认图片 (可以是 File 对象或者一个路径)
    title: '裁剪', // 弹框标题
    croppedCanvasOptions: () => ({}), // 生成图片的配置
    cropperOptions: () => ({
      viewMode: 1, // 裁剪模式 0 无限制;1 限制裁剪框不能超出图片的范围;2 限制裁剪框不能超出图片的范围 且图片填充模式为 cover 最长边填充;3 限制裁剪框不能超出图片的范围 且图片填充模式为 contain 最短边填充
      aspectRatio: 1, // 设置裁剪框为固定的宽高比
      dragMode: 'move', // 拖拽图片模式, crop 形成新的裁剪框; move 图片可移动; none 什么也没有
      preview: '#image-preview', //  预览 设置一个区域容器预览裁剪后的结果
      initialAspectRatio: 1, // 裁剪框宽高比的初始值 默认与图片宽高比相同 只有在aspectRatio没有设置的情况下可用
      data: undefined, // 之前存储的裁剪后的数据 在初始化时会自动设置 只有在autoCrop设置为true时可用
      responsive: true, //  在窗口尺寸调整后 进行响应式的重渲染 默认true
      restore: true, // 在窗口尺寸调整后 恢复被裁剪的区域 默认true
      checkCrossOrigin: true, // 检查图片是否跨域 默认true 如果是 会在被复制的图片元素上加上属性crossOrigin 并且在src上加上一个时间戳 避免重加载图片时因为浏览器缓存而加载错误
      checkOrientation: true, // 检查图片的方向信息（仅JPEG图片有）默认true 在旋转图片时会对图片方向值做一些处理 以解决IOS设备上的一些问题
      modal: true, //是否显示图片和裁剪框之间的黑色蒙版 默认true
      guides: true, // 是否显示裁剪框的虚线 默认true
      center: true, // 是否显示裁剪框中间的 ‘+’ 指示器 默认true
      highlight: true, // 是否显示裁剪框上面的白色蒙版 （很淡）默认true
      background: true, // 是否在容器内显示网格状的背景 默认true
      autoCrop: true, // 允许初始化时自动的裁剪图片 配合 data 使用 默认true
      autoCropArea: 0.8, // 设置裁剪区域占图片的大小 值为 0-1 默认 0.8 表示 80%的区域
      movable: true, // 是否可以移动图片 默认true
      rotatable: true, // 是否可以旋转图片 默认true
      scalable: true, // 是否可以缩放图片（可以改变长宽） 默认true
      zoomable: true, // 是否可以缩放图片（改变焦距） 默认true
      zoomOnTouch: true, // 是否可以通过拖拽触摸缩放图片 默认true
      zoomOnWheel: true, // 是否可以通过鼠标滚轮缩放图片 默认true
      wheelZoomRatio: 0.1, // 设置鼠标滚轮缩放的灵敏度 默认 0.1
      cropBoxMovable: true, // 是否可以拖拽裁剪框 默认true
      cropBoxResizable: true, // 是否可以改变裁剪框的尺寸 默认true
      toggleDragModeOnDblclick: true, // 是否可以通过双击切换拖拽图片模式（move和crop）默认true 当拖拽图片模式为none时不可切换 该设置必须浏览器支持双击事件
      minContainerWidth: 200, // 容器的最小宽度 默认200
      minContainerHeight: 200, // 容器的最小 高度 默认200
      minCanvasWidth: 0, // 画布的最小宽度 默认0
      minCanvasHeight: 0, // 画布的最小高度 默认0
      minCropBoxWidth: 0, //  裁剪框小宽度 默认0
      minCropBoxHeight: 0, //  裁剪框小高度 默认0
    }),
  });
  const fileTypeObj = {
    'jpg': 'image/jpeg',
    'png': 'image/png',
    'webp': 'image/png',
  };
  /** 是否已经创建了图片 */
  const isCreate = ref(false);
  /** 裁剪对象 */
  let cropper: Cropper | null = null;
  /** 裁剪的路径 */
  const imgUrl = ref('');
  /** 图片对象 */
  const imageRef = ref<HTMLImageElement | null>(null);
  /** 裁剪高度 */
  const cropperHeight = ref<number | null>(null);
  /** 裁剪宽度 */
  const cropperWidth = ref<number | null>(null);
  /** 是否显示弹窗 */
  const showDialog = ref(false);
  /** input标签 */
  const fileRef = ref<HTMLInputElement | null>(null);
  /** 裁剪配置 */
  const cropperOptions = shallowRef<Props['cropperOptions']>(props.cropperOptions);
  /** 裁剪配置 */
  const croppedCanvasOptions = shallowRef<Props['croppedCanvasOptions']>(props.croppedCanvasOptions);
  /** 标题 */
  const title = ref(props.title);
  /** 成功回调 */
  let success: ShowParam['success'] | undefined = undefined;
  /** 失败回调 */
  let fail: ShowParam['fail'] | undefined = undefined;
  /** 打开弹窗 */
  function show(param?: ShowParam) {
    const _param = param || ({} as ShowParam);
    success = _param?.success;
    fail = _param?.fail;
    title.value = _param.title || props.title;
    _param.cropperOptions = _param.cropperOptions || ({} as Props['cropperOptions']);
    _param.croppedCanvasOptions = _param.croppedCanvasOptions || ({} as Props['croppedCanvasOptions']);
    cropperOptions.value = { ...props.cropperOptions, ..._param.cropperOptions };
    croppedCanvasOptions.value = { ...props.croppedCanvasOptions, ..._param.croppedCanvasOptions };
    showDialog.value = true;
    nextTick(() => {
      const file = _param.file || props.file;
      if (file) {
        initCrop(file);
      }
    });
  }

  /** 关闭弹窗 */
  function hide() {
    showDialog.value = false;
    imgUrl.value = '';
    cropper && cropper.destroy();
    fail && fail();
  }
  /** 选择图片 */
  function chooseImage() {
    // 当input的type属性值为file时，点击方法可以进行选取文件
    fileRef.value && fileRef.value.click();
  }

  /** 提交 */
  function submit() {
    try {
      if (cropper) {
        const fileType: FileType = croppedCanvasOptions.value?.fileType || 'jpg';
        const fileName = croppedCanvasOptions.value?.fileName || `${Date.now()}.${fileType}`;
        const quality = croppedCanvasOptions.value?.quality || 0.7;

        const cas = cropper.getCroppedCanvas(croppedCanvasOptions.value);
        const toBlobCallback = (blob: Blob | null) => {
          success && success(new File([blob!], fileName, { type: fileTypeObj[fileType] }));
          showDialog.value = false;
        };
        cas.toBlob(toBlobCallback, fileTypeObj[fileType], quality);
      } else {
        showDialog.value = false;
      }
    } catch (error) {
      showDialog.value = false;
    }
  }

  /**
   * 选择图片
   */
  function selectImage(event: Event) {
    const target = event.target as HTMLInputElement;
    initCrop(target.files![0]!);
  }

  /**
   * 初始化裁剪
   */
  function initCrop(file: File | string) {
    if (file instanceof File) {
      const URL = window.URL || window.webkitURL;
      imgUrl.value = URL.createObjectURL(file);
    } else {
      imgUrl.value = file;
    }
    if (imgUrl.value) {
      openCropImage();
      isCreate.value = true;
    }
  }

  /**
   * 裁剪图片
   */
  function openCropImage() {
    nextTick(() => {
      cropper && cropper.destroy();
      if (imageRef.value) {
        cropper = new Cropper(imageRef.value, {
          ...cropperOptions.value,
          crop(event) {
            cropperHeight.value = Math.floor(event.detail.height);
            cropperWidth.value = Math.floor(event.detail.width);
          },
        });

        isCreate.value = true;
      }
    });
  }

  /**
   * 旋转
   */
  function rotateImage(angle: number) {
    isCreate.value && cropper && cropper.rotate(angle);
  }
  /**
   * 缩放
   */
  function zoomImage(num: number) {
    isCreate.value && cropper && cropper.zoom(num);
  }

  defineExpose({
    /** 显示 */
    show,
    /** 隐藏 */
    hide,
  });
</script>

<style scoped lang="scss">
  .container {
    display: flex;
    width: 100%;
    padding: 5px;
    padding-bottom: 0;

    .left-box {
      flex: 1;
      height: 100%;

      .cropper-box {
        width: 500px;
        height: 500px;
        border-radius: 4px;
        box-shadow: 0 0 0 1px var(--el-border-color) inset;

        .image-box {
          display: block;
          width: 100%;
          height: 100%;
        }
      }

      .tool-box {
        display: flex;
        align-items: center;
        justify-content: flex-start;
        width: 100%;
        margin-top: 15px;
        font-size: 10px;
      }
    }

    .right-box {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: space-between;
      width: 230px;
      height: 500px;
      padding: 0 15px;
      font-size: 14px;

      .image-preview {
        width: 200px;
        height: 200px;
        overflow: hidden;
      }

      .image-info-box {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        width: 200px;
      }
    }
  }
</style>
